package com.hzw.saas.common.security.filter;

import cn.hutool.core.date.SystemClock;
import cn.hutool.core.text.StrFormatter;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.servlet.ServletUtil;
import com.fasterxml.jackson.core.type.TypeReference;
import com.hzw.saas.common.security.constants.SecurityConstants;
import com.hzw.saas.common.util.IPHelper;
import com.hzw.saas.common.util.Json;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.util.StringUtils;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;

/**
 * 登录过滤器
 *
 * @author zzl
 * @since 12/21/2020
 */
public abstract class AbstractLoginAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    // 默认用户名密码参数名称
    private final String usernameParameter = SecurityConstants.SPRING_SECURITY_FORM_USERNAME_KEY;
    private final String passwordParameter = SecurityConstants.SPRING_SECURITY_FORM_PASSWORD_KEY;

    private final boolean log2DB = true;
    private final boolean postOnly = true;
    private final String requestMethod = "POST";

    protected PasswordEncoder passwordEncoder;


    public AbstractLoginAuthenticationFilter(String defaultFilterProcessesUrl) {
        super(defaultFilterProcessesUrl);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        // 判断请求方式登录URL 是否符合
        if (postOnly && !ServletUtil.METHOD_POST.equals(request.getMethod())) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        // 登录需要的参数
        String username, password;
        if (MediaType.APPLICATION_JSON_VALUE.equals(request.getContentType()) || MediaType.APPLICATION_JSON_UTF8_VALUE.equals(request.getContentType())) {
            // 解析 Body数据 Json
            Map<String, String> loginData = Json.getObjectMapper().readValue(request.getInputStream(), new TypeReference<Map<String, String>>() {});
            username = String.valueOf(loginData.get(usernameParameter));
            password = String.valueOf(loginData.get(passwordParameter));
        } else {
            username = String.valueOf(request.getParameter(usernameParameter));
            password = String.valueOf(request.getParameter(passwordParameter));
        }
        logger.trace(StrFormatter.format("登录信息：{username: {}, password: {}}", username, password));

        long preInvokeTimestamp = this.preInvoke(username);
        try {
            Authentication authentication = this.handleAuth(request, username, password);
            this.postInvoke(preInvokeTimestamp, username, null);
            return authentication;
        } catch (AuthenticationException | IOException | ServletException e) {
            this.postInvoke(preInvokeTimestamp, username, e.getMessage());
            throw e;
        }
    }

    /**
     * 校验密码，生成token
     */
    private Authentication handleAuth(HttpServletRequest request, String username, String password) throws AuthenticationException, IOException, ServletException {
        if (!StringUtils.hasText(username) || !StringUtils.hasText(password)) {
            throw new BadCredentialsException("无法获取用户输入的凭证信息!");
        }
        UserDetails user = loadUserByUsername(username);
        String encodedPassword = user.getPassword();
        // 密码不正确
        if (!passwordEncoder.matches(password, encodedPassword)) {
            throw new BadCredentialsException("用户名或密码错误");
        }
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(user, password, user.getAuthorities());
        token.setDetails(new WebAuthenticationDetails(request));
        return token;
    }

    private long preInvoke(String username) {
        logger.info(StrFormatter.format("request start: {}({}) -> {} {}", username, this.getIp(), requestMethod, SecurityConstants.LOGIN_URI));
        return SystemClock.now();
    }

    private void postInvoke(long preInvokeTimestamp, String username, String errorMsg) {
        // 执行时长(毫秒)
        long time = SystemClock.now() - preInvokeTimestamp;
        if (StrUtil.isBlank(errorMsg)) {
            logger.info(StrFormatter.format("request over: {}({}) => {} {} , finish in {}ms",
                username, this.getIp(), requestMethod, SecurityConstants.LOGIN_URI, time));
        } else {
            logger.warn(StrFormatter.format("request over: {}({}) => {} {} , finish in {}ms with exception: {}",
                username, this.getIp(), requestMethod, SecurityConstants.LOGIN_URI, time, errorMsg));
        }
        if (log2DB) {
            this.saveDbLog(time, username, errorMsg);
        }
    }

    protected String getIp() {
        String ip = "";
        try {
            ip = IPHelper.getIpAddr();
        } catch (Exception ignored) {}
        return ip;
    }

    protected String getRequestMethod() {
        return requestMethod;
    }

    /**
     * 查询用户逻辑
     * @param username
     * @return
     */
    protected abstract UserDetails loadUserByUsername(String username);

    /**
     * 保存用户登录操作
     * @param time
     * @param username
     * @param errorMsg
     */
    protected abstract void saveDbLog(long time, String username, String errorMsg);


    protected abstract void setPasswordEncoder(PasswordEncoder passwordEncoder);
}
